# -*- coding: utf-8 -*-


import time, math

from collections import defaultdict

import _feature_  as feature
import _read_xml_ as read_xml
import _conf_     as conf

from _perceptron_ import *


# -----------------------------------------------------------------------------
# averaging weights. (binary)
# -----------------------------------------------------------------------------
def averageWeights(weights, w_0, w_a, data_num):
    for key in weights.keys():
        weights[key] = w_0[key] - w_a[key]/data_num


# -----------------------------------------------------------------------------
# get output of binary classification with averaged-perceptron.
# [INPUT]
#     inputs   : input vector (float dict)
#     weights  : weight vector (float dict)
#     gold     : gold label (boolean)
#     w_0      : sum_0_n{delta}
#     w_a      : sum_0_i{sum_i_n{delta}}
#     data_num : for average
# [OUTPUT]
#     (bool) : whether are weights updated
# -----------------------------------------------------------------------------
def getNewWeights(inputs, weights, w_0, w_a, gold, data_num):
    global eta

    averageWeights(weights, w_0, w_a, data_num)
    output = getOutput(inputs, weights)

    if (output > 0) != gold:
        t = +1 if gold else -1

        for key, w in weights.iteritems():
            if inputs.has_key(key):
                delta = inputs[key] * t * eta
                w_0[key] += delta
                w_a[key] += delta * data_num

        do_update = True
    
    else:
        do_update = False
    
    return do_update


# -----------------------------------------------------------------------------
# get output of binary classification with averaged-perceptron.
# [INPUT]
#     inputs  : input vector (float dict)
#     weights : weight vector (float dict)
# -----------------------------------------------------------------------------
def getOutput(inputs, weights):
    output = 0
    
    for key, x in inputs.iteritems():
        output += weights[key] * x

    return output


# -----------------------------------------------------------------------------
# output weights as model-data.
# -----------------------------------------------------------------------------
def writeWeights(filename, weights):
    with open(filename, 'w') as fo:
        for key, w in weights.iteritems():
            fo.write('%s:%lf\n' % (key, w))


# -----------------------------------------------------------------------------
# load model-data.
# -----------------------------------------------------------------------------
def loadWeights(filename):
    weights = defaultdict(float)
    
    with open(filename) as fi:
        for line in fi:
            spl = line.rsplit(':', 1)
            if spl:
                weights[spl[0]] = float(spl[1])

    return weights


# -----------------------------------------------------------------------------
# convert data to input-vector and gold-label.
# -----------------------------------------------------------------------------
def convertData(data_list, filename_feat):
    # output the feature file
    feature.writeFeatures(filename_feat, data_list)

    inputs_list = []
    gold_list   = []

    # parse .f file
    with open(filename_feat) as fi:
        for i, line in enumerate(fi):
            spl = line.split()
            gold = (spl[0] != '-1')
            x = dict()

            for s in spl[1:]:
                t = s.rsplit(':', 1)
                if len(t) == 2:
                    x[t[0]] = float(t[1])
                else:
                    x[s] = 1.0
            
            inputs_list.append(x)
            gold_list.append(gold)

    return inputs_list, gold_list


# -----------------------------------------------------------------------------
# train on data_list.
# -----------------------------------------------------------------------------
def train(data_list):
    global max_iter, period, epsilon, eta
    global fn_dev_f, fn_test_f, fn_model, fn_dev_log, fn_test_log

    initialize()
    
    weights = defaultdict(float)
    w_0     = defaultdict(float)
    w_a     = defaultdict(float)
    
    inputs_list, gold_list = convertData(data_list, fn_dev_f)

    print 'Number of instance:', len(inputs_list)
    print ''

    print 'max_iterations:', max_iter
    print 'period:',         period
    print 'epsilon:',        epsilon
    print 'eta:',            eta
    print ''

    # training
    training_start_time = time.time()
    data_num  = 1
    end_count = 0
    loss_log  = []
    
    for i in xrange(max_iter):

        print '***** Iteration #%d *****' % (i+1)
        
        start_time = time.time()
        loss = 0
        
        for j, inputs in enumerate(inputs_list):
            if getNewWeights(inputs, weights, w_0, w_a, gold_list[j], data_num):
                loss += 1
            data_num += 1

        end_time = time.time()

        if loss == 0:
            end_count += 1
        else:
            end_count = 0
        
        print 'Loss:', loss
        print 'Feature L2-norm:', sum([w*w for w in weights.values()])
        print 'Seconds required for this iteration: %lf\n' % (end_time-start_time)

        # judge convergence
        if i >= period:
            variance = getVariance(loss_log)
            
            if variance/max(1,loss) < epsilon:
                print 'Terminated with the stopping criterion'
                break

        loss_log.append(loss)
        if len(loss_log) > period:
            del loss_log[0]

    training_end_time = time.time()

    print 'Seconds required: %lf' % (training_end_time - training_start_time)

    averageWeights(weights, w_0, w_a, data_num)
    writeWeights(fn_model, weights)


# -----------------------------------------------------------------------------
# test on data_list.
# [INPUT]
#     data_list : target to test (Pair list)
# -----------------------------------------------------------------------------
def test(data_list):
    global max_iter, period, epsilon
    global fn_dev_f, fn_test_f, fn_model, fn_dev_log, fn_test_log
    
    print 'start test:'
    print '-' * 40
    
    weights = loadWeights(fn_model)
    inputs_list, gold_list = convertData(data_list, fn_test_f)

    print 'Number of instance:', len(inputs_list)
    print ''

    results = []
        
    for i, inputs in enumerate(inputs_list):
        score = getOutput(inputs, weights)
        prob_id = data_list[i].attr['id']
        label = 'Y' if score>0 else 'N'

        if conf.do_apply_sigmoid:
            score = 1.0 / (1.0 + math.exp(-1.0 * score))
            if label == 'N':
                score = 1.0 - score

        results.append('%s %s %lf' % (prob_id, label, score))

    print '-' * 40
    print 'complete test.'

    results_str = '\n'.join(results)

    with open(fn_test_log, 'w') as fo:
        fo.write(results_str)

    return results_str


# -----------------------------------------------------------------------------
# do cross-validations.
# -----------------------------------------------------------------------------
def crossvalidate():
    global max_iter, period, epsilon
    global fn_dev_f, fn_test_f, fn_model, fn_dev_log, fn_test_log
    
    initialize()

    # make feature files
    data_list = conf.getDevData()
    size      = len(data_list) / conf.sep_num

    print 'start cross-validation :'
    print '-' * 40

    result_list = []

    for i in xrange(conf.sep_num):
        print 'cross-validation (%d/%d):' % (i+1, conf.sep_num)
        print '-' * 40

        idx = size * (i+1) if i < conf.sep_num-1 else len(data_list)
        
        data_list_dev  = data_list[:size*i] + data_list[idx:]
        data_list_test = data_list[size*i:idx]

        dn = conf.log_dir_name
        fn_dev_f    = '%s/cross_%d_dev.f'    % (dn, i)
        fn_test_f   = '%s/cross_%d_test.f'   % (dn, i)
        fn_model    = '%s/cross_%d.model'    % (dn, i)
        fn_dev_log  = '%s/cross_%d_dev.log'  % (dn, i)
        fn_test_log = '%s/cross_%d_test.log' % (dn, i)

        max_iter = int(conf.params['max_iterations']) \
                   if conf.params.has_key('max_iterations') else 1000
        period  = int(conf.params['period']) \
                  if conf.params.has_key('period') else 20
        epsilon = float(conf.params['epsilon']) \
                  if conf.params.has_key('epsilon') else 0.001
        
        train(data_list_dev)
        result = test(data_list_test)
        result_list.append(result)

        print '-' * 40

    print '-' * 40
    print 'complete cross-validation.'

    fn_log = '%s/cross_test_all.log' % conf.log_dir_name
    with open(fn_log, 'w') as fo:
        fo.write('\n'.join(result_list))

